<?php
declare (strict_types=1);
/**
 * +----------------------------------------------------------------------
 * | 功能扩展插件安装类
 * | 原点 2023年3月6日16:27:33
 * | 大概一个插件的形式
 * +----------------------------------------------------------------------
 */

namespace addons;

use app\common\model\Menu;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use think\Exception;
use think\facade\Db;

class Plugin
{
    private string $api = 'http://v2.bianmin.zhiju002.com/';

    private string $apiUrl;

    private string $apiInfoUrl;

    private string $downloadUrl;

    private string $installedPath;

    private string $resourcePath;

    private string $slash = DIRECTORY_SEPARATOR;

    public function __construct()
    {
        $this->installedPath = app()->getRootPath() . 'addons' . $this->slash;
        $this->resourcePath = app()->getRootPath() . 'public' . $this->slash . 'static' . $this->slash . 'admin' . $this->slash;
        $this->apiUrl = $this->api . 'index/addons/index';
        $this->apiInfoUrl = $this->api . 'index/addons/info';
        $this->downloadUrl = $this->api . 'addons/';
    }

    public function getList(): array
    {
        try {
            $param = request()->get();
            //读取已经安装的插件
            $installed = $this->getInstalled();
            //读取线上插件
            $addons = $this->getPlugin('', isset($param['type']) ? $param['type'] : '');
            //匹配安装版本
            foreach ($addons as $k => &$v) {
                if (isset($installed[$v['key']])) {
                    $v['installedVersion'] = $installed[$v['key']]['version'];
                    if ($v['version'] != $installed[$v['key']]['version']) {
                        $v['install'] = 2;
                    } else {
                        $v['install'] = 1;
                    }
                    if (isset($installed[$v['key']]['locking']) && $installed[$v['key']]['locking'] == 1) {
                        unset($addons[$k]);
                    }
                } else {
                    $v['install'] = 0;
                    $v['installedVersion'] = '';
                }
            }
            return ['code' => 0, 'count' => 1, 'data' => $addons];
        } catch (\Exception $e) {
            return ['code' => -1, 'msg' => $e->getMessage()];
        }

    }

    public function install($param): array
    {
        try {
            if (!env('APP_DEBUG')) {
                return ['code' => -1, 'msg' => '非Debug环境，无法操作'];
            }
            //拉取远程插件版本
            $addons = $this->getPlugin($param['id']);
            //定义插件明细
            $name = $addons['key'];
            $format = $addons['format'];
            $completeName = $name . $format;
            $filename = $this->installedPath . $completeName;
            //下载插件
            if (!$this->download($filename, $completeName)) {
                return ['code' => -1, 'msg' => '插件下载失败'];
            }
            //解压插件
            $zip = new \ZipArchive;
            if ($zip->open($filename) === true) {
                //将压缩包文件解压到test目录下
                $zip->extractTo($this->installedPath . $name);
                // 关闭zip文件
                $zip->close();
            } else {
                return ['code' => -1, 'msg' => '文件解压失败，请稍后再试'];
            }
            //删除旧包
            unlink($filename);

            //更新本地插件
            $this->setInstalled($name, $addons);

            if ($addons['type'] == 1) {
                //读取是否有Facade
                $this->copyFacade($name);
                //读取是否有config
                $this->copyConfig($name);
                //读取其他
                $this->copyCode($name, $addons);
            } else {
                //读取是否有config
                $this->copyConfig($name);
                $this->copyCode($name, $addons);
            }
            return ['code' => 1, 'msg' => '安装成功'];
        } catch (\Exception $e) {
            return ['code' => -1, 'msg' => $e->getMessage()];
        }
    }

    public function copyCode($name, $addons)
    {
        //列出需要读取的目录
        $dir = ['controller', 'model', 'resource', 'route', 'validate', 'view', 'config.php', 'install.sql', 'api'];
        foreach ($dir as $key => $value) {
            switch ($value) {
                case "controller":
                case "model":
                case "route":
                case "validate":
                    $form = $this->getPath($name, 'form_' . $value);
                    if (file_exists($form)) {
                        $this->copyDirs($form, $this->getPath($name, $value));
                    }
                    break;
                case "view":
                    if ($addons['view']) {
                        $form = $this->getPath($name, 'form_' . $value);
                        if (file_exists($form)) {
                            $this->copyDirs($form, $this->getPath($name, $value));
                        }
                    }
                    break;
                case "resource":
                    $resource = ['js', 'css', 'images'];
                    foreach ($resource as $str) {
                        $form = $this->getPath($name, 'form_' . $str);
                        if (file_exists($form)) {
                            $this->copyDirs($form, $this->getPath($name, $str));
                        }
                    }
                    break;
                case "config.php":
                    //创建菜单
                    $config = file_exists($this->getPath($name, 'form_config_file')) ? require $this->getPath($name, 'form_config_file') : [];
                    if (isset($config['menu'])) {
                        foreach ($config['menu'] as $k => $v) {
                            $father = Menu::create($v);
                            if (isset($v['children'])) {
                                foreach ($v['children'] as $child) {
                                    $child['pid'] = $father->id;
                                    Menu::create($child);
                                }
                            }
                        }
                    }
                    break;
                case "install.sql":
                    //导入sql
                    $sqlFile = $this->getPath($name, 'form_sql');
                    if (is_file($sqlFile)) {
                        $lines = file($sqlFile);
                        $tempLine = '';
                        foreach ($lines as $line) {
                            if (substr($line, 0, 2) == '--' || $line == '' || substr($line, 0, 2) == '/*') {
                                continue;
                            }
                            $tempLine .= $line;
                            if (substr(trim($line), -1, 1) == ';') {
                                $tempLine = str_ireplace('INSERT INTO ', 'INSERT IGNORE INTO ', $tempLine);
                                try {
                                    Db::execute($tempLine);
                                } catch (\Exception $e) {
                                    throw new Exception('Sql导入失败：' . $e->getMessage());
                                }
                                $tempLine = '';
                            }
                        }
                    }
                    break;
                case "api":
                    $apiDir = ['controller', 'route', 'validate'];
                    foreach ($apiDir as $apiValue) {
                        $form = $this->getPath($name, 'form_api_' . $apiValue);
                        if (file_exists($form)) {
                            $this->copyDirs($form, $this->getPath($name, 'api_' . $apiValue));
                        }
                    }
                    break;
            }
        }
        return true;

    }

    public function locking($param): array
    {
        try {
            //拉取远程插件版本
            $addons = $this->getPlugin($param['id']);
            if (empty($addons)) {
                return ['code' => -1, 'msg' => '插件获取失败'];
            }
            $installed = $this->getInstalled();
            $installed[$addons['key']]['locking'] = 1;
            file_put_contents($this->installedPath . 'addons.json', json_encode($installed));
            return ['code' => 1, 'msg' => '已锁定'];
        } catch (\Exception $e) {
            return ['code' => -1, 'msg' => $e->getMessage()];
        }
    }

    public function uninstall($param): array
    {
        try {
            if (!env('APP_DEBUG')) {
                return ['code' => -1, 'msg' => '非Debug环境，无法卸载'];
            }
            //拉取远程插件版本
            $addons = $this->getPlugin($param['id']);
            if ($addons['type'] == 2) {
                //删除菜单
                $config = require $this->getPath($addons['key'], 'form_config_file');
                if (isset($config['menu'])) {
                    foreach ($config['menu'] as $k => $v) {
                        $father = Menu::where(['title' => $v['title'], 'name' => $v['name']])->find();
                        if ($father) {
                            $father->delete();
                            Menu::destroy(['pid' => $father->id]);
                        }
                    }
                }
                //删除sql
                if (isset($config['table'])) {
                    foreach ($config['table'] as $table) {
                        Db::execute('DROP TABLE IF EXISTS `' . $table . '`');
                    }
                }
                //删除该插件
                $resource = ['controller', 'model', 'route', 'validate', 'view', 'api'];
                foreach ($resource as $value) {
                    if ($value == 'api' && $addons['api']) {
                        $this->deleteApi($addons);
                    } else if ($value == 'route') {
                        $files = scandir($this->getPath($addons['key'], 'form_' . $value));
                        if (is_array($files)) {
                            $to = $this->getPath($addons['key'], $value);
                            foreach ($files as $v) {
                                if ($v != '.' && $v != '..') {
                                    if (file_exists($to . $v)) {
                                        unlink($to . $v);
                                    }
                                }
                            }
                        }
                    } else {
                        $this->deleteDir($this->getPath($addons['key'], $value));
                    }
                }
                //删除该插件Resource
                $this->deleteDir($this->resourcePath . 'addons' . $this->slash . strtolower($addons['key']) . $this->slash);
                //删除该插件Config
                $this->copyConfig($addons['key'], true);
                //删除该插件相关目录所有文件
                $this->deleteDir($this->installedPath . $addons['key']);
            } else {
                //删除该插件Facade
                $this->copyFacade($addons['key'], true);
                //删除该插件Config
                $this->copyConfig($addons['key'], true);
                //删除该插件相关目录所有文件
                $this->deleteDir($this->installedPath . $addons['key']);
            }

            //更新本地插件列表
            $this->setInstalled($addons['key'], '', true);

            return ['code' => 1, 'msg' => '卸载成功'];
        } catch (\Exception $e) {
            return ['code' => -1, 'msg' => $e->getMessage()];
        }
    }

    public function deleteApi($addons)
    {
        $apiDir = ['controller', 'route', 'validate'];
        foreach ($apiDir as $apiValue) {
            if ($apiValue == 'route') {
                $files = scandir($this->getPath($addons['key'], 'form_api_' . $apiValue));
                if (is_array($files)) {
                    $to = $this->getPath($addons['key'], 'api_' . $apiValue);
                    foreach ($files as $v) {
                        if ($v != '.' && $v != '..') {
                            if (file_exists($to . $v)) {
                                unlink($to . $v);
                            }
                        }
                    }
                }
            } else {
                $this->deleteDir($this->getPath($addons['key'], 'api_' . $apiValue));
            }
        }
    }

    public function copyFacade($name, $del = false)
    {
        $to = $this->getPath($name, 'facade');
        if ($del) {
            if (file_exists($to)) {
                unlink($to);
            }
            return true;
        }
        $from = $this->getPath($name, 'form_facade');
        if (!file_exists($from)) return true;
        if (!copy($from, $to)) {
            throw new Exception('复制Facade文件错误，请尝试手动复制');
        }
        return true;
    }

    public function copyConfig($name, $del = false)
    {
        $to = $this->getPath($name, 'config');
        if ($del) {
            if (file_exists($to)) {
                unlink($to);
            }
            return true;
        }
        $from = $this->getPath($name, 'form_config');
        if (!file_exists($from)) return true;
        if (!copy($from, $to)) {
            throw new Exception('复制Config文件错误，请尝试手动复制');
        }
        return true;
    }

    public function copyResource($name)
    {
        $resource = ['js', 'css', 'images'];
        foreach ($resource as $key => $value) {
            $form = $this->getPath($name, 'form' . $value);
            if (file_exists($form)) {
                $toPath = $this->getPath($name, strtolower($value));
                if (!file_exists($toPath)) {
                    mkdir($toPath, 0755, true);
                }
                $this->copyDirs($form, $this->getPath($name, strtolower($value)));
            }
        }
    }

    public function getPath($name, $type)
    {
        switch ($type) {
            case "form_js":
                $path = $this->installedPath . $name . $this->slash . 'resource' . $this->slash . 'js' . $this->slash;
                break;
            case "form_css":
                $path = $this->installedPath . $name . $this->slash . 'resource' . $this->slash . 'css' . $this->slash;
                break;
            case "form_images":
                $path = $this->installedPath . $name . $this->slash . 'resource' . $this->slash . 'images' . $this->slash;
                break;
            case "form_facade":
                $path = $this->installedPath . $name . $this->slash . 'facade' . $this->slash . $name . '.php';
                break;
            case "form_config":
                $path = $this->installedPath . $name . $this->slash . 'config' . $this->slash . lcfirst($name) . '.php';
                break;
            case "form_controller":
                $path = $this->installedPath . $name . $this->slash . 'controller' . $this->slash;
                break;
            case "form_validate":
                $path = $this->installedPath . $name . $this->slash . 'validate' . $this->slash;
                break;
            case "form_model":
                $path = $this->installedPath . $name . $this->slash . 'model' . $this->slash;
                break;
            case "form_view":
                $path = $this->installedPath . $name . $this->slash . 'view' . $this->slash;
                break;
            case "form_route":
                $path = $this->installedPath . $name . $this->slash . 'route' . $this->slash;
                break;
            case "form_sql":
                $path = $this->installedPath . $name . $this->slash . 'install.sql';
                break;
            case "form_config_file":
                $path = $this->installedPath . $name . $this->slash . 'config.php';
                break;
            case "form_api_controller":
                $path = $this->installedPath . $name . $this->slash . 'api' . $this->slash . 'controller' . $this->slash;
                break;
            case "form_api_validate":
                $path = $this->installedPath . $name . $this->slash . 'api' . $this->slash . 'validate' . $this->slash;
                break;
            case "form_api_route":
                $path = $this->installedPath . $name . $this->slash . 'api' . $this->slash . 'route' . $this->slash;
                break;
            case "facade":
                $path = app()->getBasePath() . 'common' . $this->slash . 'facade' . $this->slash . $name . '.php';
                break;
            case "config":
                $path = app()->getRootPath() . 'config' . $this->slash . lcfirst($name) . '.php';
                break;
            case "controller":
                $path = app()->getBasePath() . 'admin' . $this->slash . 'controller' . $this->slash . 'addons' . $name . $this->slash;
                break;
            case "validate":
                $path = app()->getBasePath() . 'admin' . $this->slash . 'validate' . $this->slash . 'addons' . $name . $this->slash;
                break;
            case "model":
                $path = app()->getBasePath() . 'common' . $this->slash . 'model' . $this->slash . 'addons' . $name . $this->slash;
                break;
            case "view":
                $path = app()->getRootPath() . 'view' . $this->slash . 'admin' . $this->slash . 'addons_' . strtolower($name) . $this->slash;
                break;
            case "route":
                $path = app()->getBasePath() . 'admin' . $this->slash . 'route' . $this->slash;
                break;
            case "api_controller":
                $path = app()->getBasePath() . 'api' . $this->slash . 'controller' . $this->slash . 'addons' . $name . $this->slash;
                break;
            case "api_validate":
                $path = app()->getBasePath() . 'api' . $this->slash . 'validate' . $this->slash . 'addons' . $name . $this->slash;
                break;
            case "api_route":
                $path = app()->getBasePath() . 'api' . $this->slash . 'route' . $this->slash;
                break;
            case "js":
            case "css":
            case "images":
                $path = $this->resourcePath . 'addons' . $this->slash . strtolower($name) . $this->slash . $type . $this->slash;
                break;
            default:
                $path = '';
        }
        return $path;
    }

    public function getPlugin($id = '', $type = '')
    {
        if ($id) {
            $plugin = file_get_contents($this->apiInfoUrl . '?id=' . $id);
        } else {
            $plugin = file_get_contents($this->apiUrl . '?type=' . $type);
        }
        if (!$plugin) {
            throw new Exception('读取插件失败，请稍后再试');
        }
        $plugin = json_decode($plugin, true)['data'];

        return $plugin;
    }

    public function download($filename, $completeName)
    {
        return file_put_contents($filename, file_get_contents($this->downloadUrl . $completeName));
    }

    public function getInstalled()
    {
        if (!file_exists($this->installedPath . 'addons.json')) {
            throw new Exception('读取本地插件失败，请检查addons/addons.json');
        }
        return json_decode(file_get_contents($this->installedPath . 'addons.json'), true);
    }

    public function setInstalled($name, $addons = '', $del = false)
    {
        $installed = $this->getInstalled();
        if ($del) {
            if (isset($installed[$name])) unset($installed[$name]);
        } else {
            $installed[$name] = $addons;
            $installed[$name]['update_time'] = time();
        }
        return file_put_contents($this->installedPath . 'addons.json', json_encode($installed));
    }

    public function deleteDir($path)
    {
        if (is_dir($path)) {
            //扫描一个目录内的所有目录和文件并返回数组
            $dirs = scandir($path);
            foreach ($dirs as $dir) {
                //排除目录中的当前目录(.)和上一级目录(..)
                if ($dir != '.' && $dir != '..') {
                    //如果是目录则递归子目录，继续操作
                    $sonDir = $path . '/' . $dir;
                    if (is_dir($sonDir)) {
                        $this->deleteDir($sonDir);
                        @rmdir($sonDir);
                    } else {

                        @unlink($sonDir);
                    }
                }
            }
            @rmdir($path);
        }
    }

    public function copyDirs($source, $dest)
    {
        try {
            if (!is_dir($dest)) {
                mkdir($dest, 0755, true);
            }
            foreach ($iterator = new RecursiveIteratorIterator(
                new RecursiveDirectoryIterator($source, RecursiveDirectoryIterator::SKIP_DOTS),
                RecursiveIteratorIterator::SELF_FIRST
            ) as $item) {
                if ($item->isDir()) {
                    $subDir = $dest . DIRECTORY_SEPARATOR . $iterator->getSubPathName();
                    if (!is_dir($subDir)) {
                        mkdir($subDir, 0755, true);
                    }
                } else {
                    copy($item->getPathName(), $dest . DIRECTORY_SEPARATOR . $iterator->getSubPathName());
                }
            }
            return true;
        } catch (\Exception $e) {
            return false;
        }
    }

}